// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace CommunityToolkit.Mvvm.ComponentModel;

/// <summary>
/// A base class for objects implementing the <see cref="INotifyDataErrorInfo"/> interface. This class
/// also inherits from <see cref="ObservableObject"/>, so it can be used for observable items too.
/// </summary>
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
{
    /// <summary>
    /// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track compiled delegates to validate entities.
    /// </summary>
    private static readonly ConditionalWeakTable<Type, Action<object>> EntityValidatorMap = new();

    /// <summary>
    /// The <see cref="ConditionalWeakTable{TKey, TValue}"/> instance used to track display names for properties to validate.
    /// </summary>
    /// <remarks>
    /// This is necessary because we want to reuse the same <see cref="ValidationContext"/> instance for all validations, but
    /// with the same behavior with respect to formatted names that new instances would have provided. The issue is that the
    /// <see cref="ValidationContext.DisplayName"/> property is not refreshed when we set <see cref="ValidationContext.MemberName"/>,
    /// so we need to replicate the same logic to retrieve the right display name for properties to validate and update that
    /// property manually right before passing the context to <see cref="Validator"/> and proceed with the normal functionality.
    /// </remarks>
    private static readonly ConditionalWeakTable<Type, Dictionary<string, string>> DisplayNamesMap = new();

    /// <summary>
    /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
    /// </summary>
    private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new(nameof(HasErrors));

    /// <summary>
    /// The <see cref="ValidationContext"/> instance currently in use.
    /// </summary>
    private readonly ValidationContext validationContext;

    /// <summary>
    /// The <see cref="Dictionary{TKey,TValue}"/> instance used to store previous validation results.
    /// </summary>
    private readonly Dictionary<string, List<ValidationResult>> errors = new();

    /// <summary>
    /// Indicates the total number of properties with errors (not total errors).
    /// This is used to allow <see cref="HasErrors"/> to operate in O(1) time, as it can just
    /// check whether this value is not 0 instead of having to traverse <see cref="errors"/>.
    /// </summary>
    private int totalErrors;

    /// <inheritdoc/>
    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

    /// <summary>
    /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
    /// This constructor will create a new <see cref="ValidationContext"/> that will
    /// be used to validate all properties, which will reference the current instance
    /// and no additional services or validation properties and settings.
    /// </summary>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected ObservableValidator()
    {
        this.validationContext = new ValidationContext(this);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
    /// This constructor will create a new <see cref="ValidationContext"/> that will
    /// be used to validate all properties, which will reference the current instance.
    /// </summary>
    /// <param name="items">A set of key/value pairs to make available to consumers.</param>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected ObservableValidator(IDictionary<object, object?>? items)
    {
        this.validationContext = new ValidationContext(this, items);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
    /// This constructor will create a new <see cref="ValidationContext"/> that will
    /// be used to validate all properties, which will reference the current instance.
    /// </summary>
    /// <param name="serviceProvider">An <see cref="IServiceProvider"/> instance to make available during validation.</param>
    /// <param name="items">A set of key/value pairs to make available to consumers.</param>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected ObservableValidator(IServiceProvider? serviceProvider, IDictionary<object, object?>? items)
    {
        this.validationContext = new ValidationContext(this, serviceProvider, items);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
    /// This constructor will store the input <see cref="ValidationContext"/> instance,
    /// and it will use it to validate all properties for the current viewmodel.
    /// </summary>
    /// <param name="validationContext">
    /// The <see cref="ValidationContext"/> instance to use to validate properties.
    /// <para>
    /// This instance will be passed to all <see cref="Validator.TryValidateObject(object, ValidationContext, ICollection{ValidationResult})"/>
    /// calls executed by the current viewmodel, and its <see cref="ValidationContext.MemberName"/> property will be updated every time
    /// before the call is made to set the name of the property being validated. The property name will not be reset after that, so the
    /// value of <see cref="ValidationContext.MemberName"/> will always indicate the name of the last property that was validated, if any.
    /// </para>
    /// </param>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="validationContext"/> is <see langword="null"/>.</exception>
    protected ObservableValidator(ValidationContext validationContext)
    {
        ArgumentNullException.ThrowIfNull(validationContext);

        this.validationContext = validationContext;
    }

    /// <inheritdoc/>
    public bool HasErrors => this.totalErrors > 0;

    /// <summary>
    /// Compares the current and new values for a given property. If the value has changed,
    /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
    /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="field">The field storing the property's value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
    /// <remarks>
    /// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
    /// of the <paramref name="validate"/> parameter. If that is set to <see langword="true"/>, the new value will be
    /// validated and <see cref="ErrorsChanged"/> will be raised if needed. Following the behavior of the base method,
    /// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
    /// are not raised if the current and new value for the target property are the same.
    /// </remarks>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="propertyName"/> is <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, bool validate, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(propertyName);

        bool propertyChanged = SetProperty(ref field, newValue, propertyName);

        if (propertyChanged && validate)
        {
            ValidateProperty(newValue, propertyName);
        }

        return propertyChanged;
    }

    /// <summary>
    /// Compares the current and new values for a given property. If the value has changed,
    /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
    /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
    /// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="field">The field storing the property's value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
    /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, IEqualityComparer<T> comparer, bool validate, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(comparer);
        ArgumentNullException.ThrowIfNull(propertyName);

        bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);

        if (propertyChanged && validate)
        {
            ValidateProperty(newValue, propertyName);
        }

        return propertyChanged;
    }

    /// <summary>
    /// Compares the current and new values for a given property. If the value has changed,
    /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
    /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
    /// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
    /// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="callback">A callback to invoke to update the property value.</param>
    /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
    /// <remarks>
    /// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
    /// of the <paramref name="validate"/> parameter. As such, following the behavior of the base method,
    /// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
    /// are not raised if the current and new value for the target property are the same.
    /// </remarks>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool validate, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName);

        if (propertyChanged && validate)
        {
            ValidateProperty(newValue, propertyName);
        }

        return propertyChanged;
    }

    /// <summary>
    /// Compares the current and new values for a given property. If the value has changed,
    /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
    /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
    /// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
    /// <param name="callback">A callback to invoke to update the property value.</param>
    /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool validate, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(comparer);
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName);

        if (propertyChanged && validate)
        {
            ValidateProperty(newValue, propertyName);
        }

        return propertyChanged;
    }

    /// <summary>
    /// Compares the current and new values for a given nested property. If the value has changed,
    /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
    /// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
    /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>, with the difference being that this
    /// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for
    /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
    /// </summary>
    /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
    /// <typeparam name="T">The type of property (or field) to set.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="model">The model </param>
    /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
    /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string propertyName = null!)
        where TModel : class
    {
        ArgumentNullException.ThrowIfNull(model);
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName);

        if (propertyChanged && validate)
        {
            ValidateProperty(newValue, propertyName);
        }

        return propertyChanged;
    }

    /// <summary>
    /// Compares the current and new values for a given nested property. If the value has changed,
    /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
    /// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
    /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>,
    /// with the difference being that this method is used to relay properties from a wrapped model in the
    /// current instance. For more info, see the docs for
    /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>.
    /// </summary>
    /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
    /// <typeparam name="T">The type of property (or field) to set.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
    /// <param name="model">The model </param>
    /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
    /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string propertyName = null!)
        where TModel : class
    {
        ArgumentNullException.ThrowIfNull(comparer);
        ArgumentNullException.ThrowIfNull(model);
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName);

        if (propertyChanged && validate)
        {
            ValidateProperty(newValue, propertyName);
        }

        return propertyChanged;
    }

    /// <summary>
    /// Tries to validate a new value for a specified property. If the validation is successful,
    /// <see cref="ObservableObject.SetProperty{T}(ref T,T,string?)"/> is called, otherwise no state change is performed.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="field">The field storing the property's value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns>Whether the validation was successful and the property value changed as well.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="propertyName"/> is <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool TrySetProperty<T>(ref T field, T newValue, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(propertyName);

        return TryValidateProperty(newValue, propertyName, out errors) &&
               SetProperty(ref field, newValue, propertyName);
    }

    /// <summary>
    /// Tries to validate a new value for a specified property. If the validation is successful,
    /// <see cref="ObservableObject.SetProperty{T}(ref T,T,IEqualityComparer{T},string?)"/> is called, otherwise no state change is performed.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="field">The field storing the property's value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns>Whether the validation was successful and the property value changed as well.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool TrySetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(comparer);
        ArgumentNullException.ThrowIfNull(propertyName);

        return TryValidateProperty(newValue, propertyName, out errors) &&
               SetProperty(ref field, newValue, comparer, propertyName);
    }

    /// <summary>
    /// Tries to validate a new value for a specified property. If the validation is successful,
    /// <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string?)"/> is called, otherwise no state change is performed.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="callback">A callback to invoke to update the property value.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns>Whether the validation was successful and the property value changed as well.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool TrySetProperty<T>(T oldValue, T newValue, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        return TryValidateProperty(newValue, propertyName, out errors) &&
               SetProperty(oldValue, newValue, callback, propertyName);
    }

    /// <summary>
    /// Tries to validate a new value for a specified property. If the validation is successful,
    /// <see cref="ObservableObject.SetProperty{T}(T,T,IEqualityComparer{T},Action{T},string?)"/> is called, otherwise no state change is performed.
    /// </summary>
    /// <typeparam name="T">The type of the property that changed.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
    /// <param name="callback">A callback to invoke to update the property value.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns>Whether the validation was successful and the property value changed as well.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool TrySetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(comparer);
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        return TryValidateProperty(newValue, propertyName, out errors) &&
               SetProperty(oldValue, newValue, comparer, callback, propertyName);
    }

    /// <summary>
    /// Tries to validate a new value for a specified property. If the validation is successful,
    /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
    /// </summary>
    /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
    /// <typeparam name="T">The type of property (or field) to set.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="model">The model </param>
    /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns>Whether the validation was successful and the property value changed as well.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
        where TModel : class
    {
        ArgumentNullException.ThrowIfNull(model);
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        return TryValidateProperty(newValue, propertyName, out errors) &&
               SetProperty(oldValue, newValue, model, callback, propertyName);
    }

    /// <summary>
    /// Tries to validate a new value for a specified property. If the validation is successful,
    /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
    /// </summary>
    /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
    /// <typeparam name="T">The type of property (or field) to set.</typeparam>
    /// <param name="oldValue">The current property value.</param>
    /// <param name="newValue">The property's value after the change occurred.</param>
    /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
    /// <param name="model">The model </param>
    /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    /// <param name="propertyName">(optional) The name of the property that changed.</param>
    /// <returns>Whether the validation was successful and the property value changed as well.</returns>
    /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
        where TModel : class
    {
        ArgumentNullException.ThrowIfNull(comparer);
        ArgumentNullException.ThrowIfNull(model);
        ArgumentNullException.ThrowIfNull(callback);
        ArgumentNullException.ThrowIfNull(propertyName);

        return TryValidateProperty(newValue, propertyName, out errors) &&
               SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
    }

    /// <summary>
    /// Clears the validation errors for a specified property or for the entire entity.
    /// </summary>
    /// <param name="propertyName">
    /// The name of the property to clear validation errors for.
    /// If a <see langword="null"/> or empty name is used, all entity-level errors will be cleared.
    /// </param>
    protected void ClearErrors(string? propertyName = null)
    {
        // Clear entity-level errors when the target property is null or empty
        if (string.IsNullOrEmpty(propertyName))
        {
            ClearAllErrors();
        }
        else
        {
            ClearErrorsForProperty(propertyName!);
        }
    }

    /// <inheritdoc cref="INotifyDataErrorInfo.GetErrors(string)"/>
    public IEnumerable<ValidationResult> GetErrors(string? propertyName = null)
    {
        // Get entity-level errors when the target property is null or empty
        if (string.IsNullOrEmpty(propertyName))
        {
            // Local function to gather all the entity-level errors
            [MethodImpl(MethodImplOptions.NoInlining)]
            IEnumerable<ValidationResult> GetAllErrors()
            {
                return this.errors.Values.SelectMany(static errors => errors);
            }

            return GetAllErrors();
        }

        // Property-level errors, if any
        if (this.errors.TryGetValue(propertyName!, out List<ValidationResult>? errors))
        {
            return errors;
        }

        // The INotifyDataErrorInfo.GetErrors method doesn't specify exactly what to
        // return when the input property name is invalid, but given that the return
        // type is marked as a non-nullable reference type, here we're returning an
        // empty array to respect the contract. This also matches the behavior of
        // this method whenever errors for a valid properties are retrieved.
        return Array.Empty<ValidationResult>();
    }

    /// <inheritdoc/>
    IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) => GetErrors(propertyName);

    /// <summary>
    /// Validates all the properties in the current instance and updates all the tracked errors.
    /// If any changes are detected, the <see cref="ErrorsChanged"/> event will be raised.
    /// </summary>
    /// <remarks>
    /// Only public instance properties (excluding custom indexers) that have at least one
    /// <see cref="ValidationAttribute"/> applied to them will be validated. All other
    /// members in the current instance will be ignored. None of the processed properties
    /// will be modified - they will only be used to retrieve their values and validate them.
    /// </remarks>
    [RequiresUnreferencedCode(
        "This method requires the generated CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions type not to be removed to use the fast path. " +
        "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
        "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " +
        "Additionally, due to the usage of validation APIs, the type of the current instance cannot be statically discovered.")]
    protected void ValidateAllProperties()
    {
        // Fast path that tries to create a delegate from a generated type-specific method. This
        // is used to make this method more AOT-friendly and faster, as there is no dynamic code.
        [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
        static Action<object> GetValidationAction(Type type)
        {
            if (type.Assembly.GetType("CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType &&
                extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo)
            {
                return (Action<object>)methodInfo.Invoke(null, new object?[] { null })!;
            }

            return GetValidationActionFallback(type);
        }

        // Fallback method to create the delegate with a compiled LINQ expression
        static Action<object> GetValidationActionFallback(Type type)
        {
            // Get the collection of all properties to validate
            (string Name, MethodInfo GetMethod)[] validatableProperties = (
                from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                where property.GetIndexParameters().Length == 0 &&
                      property.GetCustomAttributes<ValidationAttribute>(true).Any()
                let getMethod = property.GetMethod
                where getMethod is not null
                select (property.Name, getMethod)).ToArray();

            // Short path if there are no properties to validate
            if (validatableProperties.Length == 0)
            {
                return static _ => { };
            }

            // MyViewModel inst0 = (MyViewModel)arg0;
            ParameterExpression arg0 = Expression.Parameter(typeof(object));
            UnaryExpression inst0 = Expression.Convert(arg0, type);

            // Get a reference to ValidateProperty(object, string)
            MethodInfo validateMethod = typeof(ObservableValidator).GetMethod(nameof(ValidateProperty), BindingFlags.Instance | BindingFlags.NonPublic)!;

            // We want a single compiled LINQ expression that validates all properties in the
            // actual type of the executing viewmodel at once. We do this by creating a block
            // expression with the unrolled invocations of all properties to validate.
            // Essentially, the body will contain the following code:
            // ===============================================================================
            // {
            //     inst0.ValidateProperty(inst0.Property0, nameof(MyViewModel.Property0));
            //     inst0.ValidateProperty(inst0.Property1, nameof(MyViewModel.Property1));
            //     ...
            //     inst0.ValidateProperty(inst0.PropertyN, nameof(MyViewModel.PropertyN));
            // }
            // ===============================================================================
            // We also add an explicit object conversion to represent boxing, if a given property
            // is a value type. It will just be a no-op if the value is a reference type already.
            // Note that this generated code is technically accessing a protected method from
            // ObservableValidator externally, but that is fine because IL doesn't really have
            // a concept of member visibility, that's purely a C# build-time feature.
            BlockExpression body = Expression.Block(
                from property in validatableProperties
                select Expression.Call(inst0, validateMethod, new Expression[]
                {
                    Expression.Convert(Expression.Call(inst0, property.GetMethod), typeof(object)),
                    Expression.Constant(property.Name)
                }));

            return Expression.Lambda<Action<object>>(body, arg0).Compile();
        }

        // Get or compute the cached list of properties to validate. Here we're using a static lambda to ensure the
        // delegate is cached by the C# compiler, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
        EntityValidatorMap.GetValue(
            GetType(),
            [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => GetValidationAction(t))(this);
    }

    /// <summary>
    /// Validates a property with a specified name and a given input value.
    /// If any changes are detected, the <see cref="ErrorsChanged"/> event will be raised.
    /// </summary>
    /// <param name="value">The value to test for the specified property.</param>
    /// <param name="propertyName">The name of the property to validate.</param>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is <see langword="null"/>.</exception>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    protected internal void ValidateProperty(object? value, [CallerMemberName] string propertyName = null!)
    {
        ArgumentNullException.ThrowIfNull(propertyName);

        // Check if the property had already been previously validated, and if so retrieve
        // the reusable list of validation errors from the errors dictionary. This list is
        // used to add new validation errors below, if any are produced by the validator.
        // If the property isn't present in the dictionary, add it now to avoid allocations.
        if (!this.errors.TryGetValue(propertyName, out List<ValidationResult>? propertyErrors))
        {
            propertyErrors = new List<ValidationResult>();

            this.errors.Add(propertyName, propertyErrors);
        }

        bool errorsChanged = false;

        // Clear the errors for the specified property, if any
        if (propertyErrors.Count > 0)
        {
            propertyErrors.Clear();

            errorsChanged = true;
        }

        // Validate the property, by adding new errors to the existing list
        this.validationContext.MemberName = propertyName;
        this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName);

        bool isValid = Validator.TryValidateProperty(value, this.validationContext, propertyErrors);

        // Update the shared counter for the number of errors, and raise the
        // property changed event if necessary. We decrement the number of total
        // errors if the current property is valid but it wasn't so before this
        // validation, and we increment it if the validation failed after being
        // correct before. The property changed event is raised whenever the
        // number of total errors is either decremented to 0, or incremented to 1.
        if (isValid)
        {
            if (errorsChanged)
            {
                this.totalErrors--;

                if (this.totalErrors == 0)
                {
                    OnPropertyChanged(HasErrorsChangedEventArgs);
                }
            }
        }
        else if (!errorsChanged)
        {
            this.totalErrors++;

            if (this.totalErrors == 1)
            {
                OnPropertyChanged(HasErrorsChangedEventArgs);
            }
        }

        // Only raise the event once if needed. This happens either when the target property
        // had existing errors and is now valid, or if the validation has failed and there are
        // new errors to broadcast, regardless of the previous validation state for the property.
        if (errorsChanged || !isValid)
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }

    /// <summary>
    /// Tries to validate a property with a specified name and a given input value, and returns
    /// the computed errors, if any. If the property is valid, it is assumed that its value is
    /// about to be set in the current object. Otherwise, no observable local state is modified.
    /// </summary>
    /// <param name="value">The value to test for the specified property.</param>
    /// <param name="propertyName">The name of the property to validate.</param>
    /// <param name="errors">The resulting validation errors, if any.</param>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    private bool TryValidateProperty(object? value, string propertyName, out IReadOnlyCollection<ValidationResult> errors)
    {
        // Add the cached errors list for later use.
        if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors))
        {
            propertyErrors = new List<ValidationResult>();

            this.errors.Add(propertyName!, propertyErrors);
        }

        bool hasErrors = propertyErrors.Count > 0;

        List<ValidationResult> localErrors = new();

        // Validate the property, by adding new errors to the local list
        this.validationContext.MemberName = propertyName;
        this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName!);

        bool isValid = Validator.TryValidateProperty(value, this.validationContext, localErrors);

        // We only modify the state if the property is valid and it wasn't so before. In this case, we
        // clear the cached list of errors (which is visible to consumers) and raise the necessary events.
        if (isValid && hasErrors)
        {
            propertyErrors.Clear();

            this.totalErrors--;

            if (this.totalErrors == 0)
            {
                OnPropertyChanged(HasErrorsChangedEventArgs);
            }

            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }

        errors = localErrors;

        return isValid;
    }

    /// <summary>
    /// Clears all the current errors for the entire entity.
    /// </summary>
    private void ClearAllErrors()
    {
        if (this.totalErrors == 0)
        {
            return;
        }

        // Clear the errors for all properties with at least one error, and raise the
        // ErrorsChanged event for those properties. Other properties will be ignored.
        foreach (KeyValuePair<string, List<ValidationResult>> propertyInfo in this.errors)
        {
            bool hasErrors = propertyInfo.Value.Count > 0;

            propertyInfo.Value.Clear();

            if (hasErrors)
            {
                ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyInfo.Key));
            }
        }

        this.totalErrors = 0;

        OnPropertyChanged(HasErrorsChangedEventArgs);
    }

    /// <summary>
    /// Clears all the current errors for a target property.
    /// </summary>
    /// <param name="propertyName">The name of the property to clear errors for.</param>
    private void ClearErrorsForProperty(string propertyName)
    {
        if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors) ||
            propertyErrors.Count == 0)
        {
            return;
        }

        propertyErrors.Clear();

        this.totalErrors--;

        if (this.totalErrors == 0)
        {
            OnPropertyChanged(HasErrorsChangedEventArgs);
        }

        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

    /// <summary>
    /// Gets the display name for a given property. It could be a custom name or just the property name.
    /// </summary>
    /// <param name="propertyName">The target property name being validated.</param>
    /// <returns>The display name for the property.</returns>
    [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
    private string GetDisplayNameForProperty(string propertyName)
    {
        static Dictionary<string, string> GetDisplayNames(Type type)
        {
            Dictionary<string, string> displayNames = new();

            foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (property.GetCustomAttribute<DisplayAttribute>() is DisplayAttribute attribute &&
                    attribute.GetName() is string displayName)
                {
                    displayNames.Add(property.Name, displayName);
                }
            }

            return displayNames;
        }

        // This method replicates the logic of DisplayName and GetDisplayName from the
        // ValidationContext class. See the original source in the BCL for more details.
        _ = DisplayNamesMap.GetValue(GetType(), static t => GetDisplayNames(t)).TryGetValue(propertyName, out string? displayName);

        return displayName ?? propertyName;
    }
}
